package com.puzzlebazar.client.heyawake;

import java.util.Arrays;
import java.util.Iterator;
import java.util.Vector;

import com.google.gwt.user.client.ui.HasText;
import com.puzzlebazar.client.puzzle.model.Move;
import com.puzzlebazar.client.puzzle.model.MoveClearPuzzle;
import com.puzzlebazar.client.puzzle.model.MoveImpl;
import com.puzzlebazar.client.puzzle.model.MoveList;
import com.puzzlebazar.client.puzzle.model.PuzzleFactory;
import com.puzzlebazar.client.squaregrid.model.MoveClearStates;
import com.puzzlebazar.client.squaregrid.model.SGModelImpl;
import com.puzzlebazar.client.squaregrid.model.StateGridImpl;
import com.puzzlebazar.client.squaregrid.model.StateGridUtils;
import com.puzzlebazar.client.squaregrid.model.StateModel;
import com.puzzlebazar.client.squaregrid.view.ComponentHighlighter;
import com.puzzlebazar.client.util.BoundInfo;
import com.puzzlebazar.client.util.Recti;
import com.puzzlebazar.client.util.Vec2i;

public final class HeyawakeModel extends SGModelImpl implements StateModel {


	/**
	 * 
	 */
	private static final long serialVersionUID = 5729505471803045738L;

	public static final class States {

		public static final int EMPTY = 0;
		public static final int FILLED = 1;
		public static final int KNOWN_EMPTY = 2;
		
	}

	public static final BoundInfo boundInfo = new BoundInfo(5,5,30,30);
	public static final Vec2i defaultSize   = new Vec2i(10,10);

	private Vector< Room > rooms = new Vector< Room > ();

	private StateGridImpl states = null;
	private StateGridImpl solutionStates = null;
	


	/**
	 * Create a heyawake model
	 */
	public HeyawakeModel() {
		super();
		createStates();
	}
	
	/**
	 * Create the main states object
	 */
	private void createStates() {
		states = new StateGridImpl( getGridSize(), getDefaultState(), getNbStates() );
	}

	@Override
	protected void setGridSize( Vec2i gridSize ) throws Exception {
		super.setGridSize(gridSize);
		
		if( states != null && !gridSize.equals(  states.getGridSize() ) ) {
			states.resize( gridSize );
			setChanged();
		}

		if( solutionStates != null && !gridSize.equals(  solutionStates.getGridSize() ) ) {
			solutionStates.resize( gridSize );
			setChanged();
		}
	}	

	
	/**
	 * Adds a new room to the model
	 * The user should make sure to call notifyObservers( moveList ) after this move,
	 * passing along the generated move list,
 	 * or notifyObservers() to refresh the entire board.
	 * 
	 * @param room The room to add
	 * @param time The time of the move
	 * @throws Exception
	 */
	public Move addRoom( Room room, long time ) throws Exception {
		if( !isRoomAreaValid(room.getRoomArea()) ) 
			throw new Exception( "Adding an invalid room" );

		Move move = new MoveAddRoom(room, time);
		rooms.add( new Room(room) );
		
		setChanged();
		
		return move;
	}


	/**
	 * Remove a room from the model
	 * The user should make sure to call notifyObservers( moveList ) after this move,
	 * passing along the generated move list,
 	 * or notifyObservers() to refresh the entire board.
	 * 
	 * @param room The room to remove
	 * @param time The time of the move
	 * @throws Exception
	 */
	public Move removeRoom( Room room, long time ) throws Exception {

		int index = findIndexOfRoomAt( room.getRoomArea() );
		if ( index < 0 ) throw new Exception( "Removing an unexisting room" );
		room = rooms.remove( index );
		Move move = new MoveRemoveRoom(room, time);
		setChanged();
		
		return move;

	}

	/**
	 * Look for a room sitting on a specified rectangle
	 * 
	 * @param rect The rectangle where a room should be located
	 * @return The index of the room found, -1 if not found
	 */
	private int findIndexOfRoomAt(Recti rect) {
		for( int i=0; i<rooms.size(); ++i){
			Room room = rooms.get(i);
			if( room.getRoomArea().equals( rect ) )
				return i;
		}
		return -1;
	}

	/**
	 * Make sure the proposed room can be added to the model
	 * 
	 * @param roomArea The room to check
	 * @return True if the room can be added to the model, false otherwise
	 * @throws Exception
	 */
	public boolean isRoomAreaValid(Recti roomArea) throws Exception {
		if( !isValidRect(roomArea) )
			throw new Exception( "Invalid room area" );
		
		for (Iterator<Room> iterator = rooms.iterator(); iterator.hasNext();) {
			Room room = iterator.next();
			if( room.getRoomArea().overlap(roomArea) ) {
				return false;
			}
		}
		
		return true;
	}

	@Override
	public boolean checkSolution() {
		if( states == null || solutionStates == null ) return false;

		int equivalenceClasses[] = {
				0,1,0
			};
		return states.match( solutionStates, equivalenceClasses );
	}

	@Override
	public Move setCurrentStateAsSolution() {
		solutionStates = states;
		createStates();
		setChanged();
		return new MoveClearStates( MoveImpl.NO_TIME );
	}
	
	/**
	 * Identify the room containing a given cell
	 * 
	 * @param cellLoc The cell location
	 * @return The Room at the specified location, or null if no room
	 */
	public Room findRoomAt(Vec2i cellLoc) throws Exception {
		if( !isValidCell(cellLoc) )
			throw new Exception( "Invalid cell location" );
		
		for (Iterator<Room> iterator = rooms.iterator(); iterator.hasNext();) {
			Room room = iterator.next();
			if( room.getRoomArea().contains(cellLoc) ) {
				return room;
			}
		}
		
		return null;
	}

	@Override
	public Move clear( long time ) {
		rooms.clear();
		if( states != null ) 
			states.clearStates( time );
		if( solutionStates != null ) 
			solutionStates.clearStates( time );
		setChanged();
		return new MoveClearPuzzle( time );
	}
	
	@Override
	public MoveList getMoveList() {
		MoveList moveList = super.getMoveList();
		appendRoomMoves(moveList);
		states.appendMoves(moveList);
		return moveList;
	}


	/**
	 * Append the moves corresponding to the rooms
	 * 
	 * @param moveList The move list to append to
	 */
	private void appendRoomMoves(MoveList moveList) {
		for (Iterator<Room> iterator = rooms.iterator(); iterator.hasNext();) {
			Room room = iterator.next();
			moveList.addMove( new MoveAddRoom(room,MoveImpl.NO_TIME) );
		}
	}

	@Override
	public boolean isSolutionValid(ComponentHighlighter highlighter, HasText textArea ) {
		
		int[][] roomIds = findRoomIds();
		
		if( !allCellsHaveOneRoom(roomIds, highlighter) ) {
			if( textArea != null )
				textArea.setText( "Some cells are not covered by exactly one room." );
			return false;
		}
		
		int[] emptyStates = new int[]{
				States.EMPTY,
				States.KNOWN_EMPTY
		};

		int[] filledStates = new int[]{
				States.FILLED
		};
		
		boolean[][] emptyCells = StateGridUtils.findCells(states, emptyStates);
		boolean[][] filledCells = StateGridUtils.findCells(states, filledStates);
		
		if( StateGridUtils.doAdjacentCellsExist(states, filledCells, highlighter) ) {
			if( textArea != null )
				textArea.setText( "Some filled cells are adjacent." );
			return false;
		}

		if( !checkRoomsCount(filledCells, highlighter) ) {
			if( textArea != null )
				textArea.setText( "Some rooms do not contain the right number of filled cells." );
			return false;			
		}
		
		if( hasEmptySequences(roomIds, emptyCells, highlighter) ) {
			if( textArea != null )
				textArea.setText( "Some sequence of empty cells cover more than two rooms." );
			return false;			
		}
		
		if( !StateGridUtils.areCellsAdjacent(states, emptyCells, highlighter) ) {
			if( textArea != null )
				textArea.setText( "Some empty cells are not connected to the rest." );
			return false;
		}
		
		return true;
	
	}



	/**
	 * Creates an array that has one value in each cell, corresponding to the unique ID of the
	 * room that contains a cell. A value of -1 indicates that no room or more than one room
	 * occupy the cell.
	 * 
	 * @return A newly created, correctly filled array
	 */
	private int[][] findRoomIds() {

		int[][] roomIds = new int[getSize().x][getSize().y];
		for( int i=0; i<roomIds.length; ++i)
			Arrays.fill( roomIds[i], -1 );
		for ( int id=0; id < rooms.size(); ++id ) {
			Room room = rooms.get(id);
			Recti area = room.getRoomArea(); 
			for( int i=area.x; i<area.x+area.w; i++ )
				for( int j=area.y; j<area.y+area.h; j++ )
					roomIds[i][j] = id;
		}
		
		return roomIds;
	}


	/**
	 * Ensures that all cells are covered by exactly one room
	 * 
	 * @param roomIds The id of the room containing each cell, <0 if not exactly one room contains the cell
	 * @param highlighter Object to use to highlight invalid cells (null for no highlighting) 
	 * @return true if all cells are covered by exactly one room, false otherwise
	 */
	private boolean allCellsHaveOneRoom(int[][] roomIds, ComponentHighlighter highlighter) {		
		
		boolean ok = true;
		Vec2i loc = new Vec2i();
		for( int i=0; i<roomIds.length; i++ ) {
			loc.x = i;
			for( int j=0; j<roomIds[i].length; j++ ) {
				loc.y = j;
				if( roomIds[i][j] < 0 ) {
					if( highlighter == null ) return false;
					ok = false;
					try {
						highlighter.highlightCell( loc );
					} catch (Exception exception) {
						exception.printStackTrace();
					}
				}
			}
		}
		
		return ok;
	}	



	/**
	 * Ensure that all numbered rooms contain the right number of filled cells 
	 * 
	 * @param filledCells Array containing true in each filled cell
	 * @param highlighter Object to use to highlight invalid cells (null for no highlighting) 
	 * @return true if all rooms have the correct number of filled cells
	 */
	private boolean checkRoomsCount(boolean[][] filledCells, ComponentHighlighter highlighter) {
		for (Iterator<Room> iterator = rooms.iterator(); iterator.hasNext();) {
			Room room = iterator.next();
			int value = room.getValue();
			if( value >= 0 ) {
				Recti area = room.getRoomArea();
				int count = 0;
				for( int i=area.x; i<area.x+area.w; i++ )
					for( int j=area.y; j<area.y+area.h; j++ )
						if( filledCells[i][j] ) count++;
				if( count != value ) {
					if( highlighter != null ) {
						try {
							highlighter.highlightCells( area.getCell00(), area.getCell11() );
						} catch (Exception exception) {
							exception.printStackTrace();
						}
					}
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Checks if some sequences of adjacent empty cells cover more than two room
	 * 
	 * @param roomIds The id of the room containing each cell
	 * @param emptyCells Array containing true in each empty cell
	 * @param highlighter Object to use to highlight invalid cells (null for no highlighting) 
	 * @return true if some sequences are detected, false otherwise
	 */
	private boolean hasEmptySequences(int[][] roomIds, boolean[][] emptyCells, ComponentHighlighter highlighter) {
		
		int sequenceStart = -1;
		int roomCount = -1;
		int lastRoom = -1;
		
		for( int i=0; i<roomIds.length; i++ ) {
			sequenceStart = -1;
			for( int j=0; j<=roomIds[i].length; j++ ) {
				if( j < roomIds[i].length && emptyCells[i][j] ) {
					if( sequenceStart == -1 ) {
						sequenceStart = j;
						roomCount = 1;
						lastRoom = roomIds[i][j];
					}
					else if( lastRoom != roomIds[i][j] ) {
						roomCount++;
						lastRoom = roomIds[i][j];
					}
				} else {
					if( roomCount > 2 ) {
						if( highlighter != null ) {
							try {
								highlighter.highlightCells( new Vec2i(i,sequenceStart), new Vec2i(i,j-1) );
							} catch (Exception exception) {
								exception.printStackTrace();
							}
						}
						return true;
					}
					sequenceStart = -1;
				}
			}
		
		}

		for( int j=0; j<roomIds[0].length; j++ ) {
			sequenceStart = -1;
			for( int i=0; i<=roomIds.length; i++ ) {
				if( i < roomIds.length && emptyCells[i][j] ) {
					if( sequenceStart == -1 ) {
						sequenceStart = i;
						roomCount = 1;
						lastRoom = roomIds[i][j];
					}
					else if( lastRoom != roomIds[i][j] ) {
						roomCount++;
						lastRoom = roomIds[i][j];
					}
				} else {
					if( roomCount > 2 ) {
						if( highlighter != null ) {
							try {
								highlighter.highlightCells( new Vec2i(sequenceStart,j), new Vec2i(i-1,j) );
							} catch (Exception exception) {
								exception.printStackTrace();
							}
						}
						return true;
					}
					sequenceStart = -1;
				}
			}
		
		}
		
		
		return false;
	}

	@Override
	public int getDefaultState() {
		return States.EMPTY;
	}

	@Override
	public int getNbStates() {
		return 3;
	}

	@Override
	public int getState(Vec2i loc) throws Exception {
		return states.getState(loc);
	}

	@Override
	public Move setState(Vec2i loc, int stateAfter, long time) throws Exception {		
		setChanged();
		return states.setState( loc, stateAfter, time );
	}

	@Override
	public Move clearStates( long time ) {
		setChanged();
		return states.clearStates( time );
	}

	@Override
	public PuzzleFactory getFactory() {
		return HeyawakeFactory.get();
	}

	@Override
	public BoundInfo getBoundInfo() {
		return boundInfo;
	}

}
